package game;

import java.util.*;

import be.kuleuven.cs.som.annotate.Basic;

/**
 * A class of CollisionPredictors that lists all the potential collisions of its source.
 * @invar	hasProperFutureCollisions()
 * @invar	isValidSource(getSource())
 * @author JeroenGillis && ThomasProvoost
 */
public class CollisionPredictor {
	
	private final FutureCollisionComparator comparator;
	private final TreeSet<FutureCollision> order;
	private final HashMap<SpaceObject, FutureCollision> components = new HashMap<SpaceObject,FutureCollision>();
	private final SpaceObject source;
	private Date boundaryCollisionDate;
	
	public CollisionPredictor(SpaceObject source) {
		this.comparator = new FutureCollisionComparator();
		this.order = new TreeSet<FutureCollision>(comparator);
		this.source = source;
		setBoundaryCollisionDate();
	}
	
	/**
	 * Set the boundaryCollisionDate of this collisionPredictor to the date when the source of this collisionPredictor is going to hit the boundary.
	 * @post	| (new this).getBoundaryCollisionDate() == (new Date()).setTime((new Date()).getTime + source.getTimeToBoundary())
	 */
	private void setBoundaryCollisionDate() {
		if(getSource().getTimeToBoundary() == Double.POSITIVE_INFINITY) 
			boundaryCollisionDate = null;
		Date boundaryCollisionDate = new Date();
		boundaryCollisionDate.setTime((long) (boundaryCollisionDate.getTime() + getSource().getTimeToBoundary()));
		this.boundaryCollisionDate = boundaryCollisionDate;
	}
	
	/**
	 * Return the date object on which the source of this collisionPredictor will hit the boundary of the world it is in.
	 */
	@Basic
	public Date getBoundaryCollisionDate() {
		return this.boundaryCollisionDate;
	}

	/**
	 * Return the final source of this collisionPredictor.
	 */
	@Basic
	public SpaceObject getSource() {
		return this.source;
	}
	
	/**
	 * Return a boolean reflecting whether the given source is a valid source for this collisionPredictor.
	 * @param source
	 * 			The spaceObject to be checked.
	 * @return	| result == (source != null)
	 */
	public boolean isValidSource(SpaceObject source) {
		return source != null;
	}
	
	/**
	 * Return a boolean reflecting whether this collisionPredictor contains a FutureCollision with reference to the given key.
	 * @param key
	 * 			The key to be checked.
	 * @return	| result == getComponents().containsKey(key)
	 */
	public boolean containsKey(Object key) {
		return getComponents().containsKey(key);
	}
	
	/**
	 * Return a boolean reflecting whether this collisionPredictor contains the given value as a FutureCollision.
	 * @param value
	 * @return	| result == getComponents().containsValue(value)
	 */
	public boolean containsValue(Object value) {
		return getComponents().containsValue(value);
	}
	
	/**
	 * Return the FutureCollision that is going to happen first. 
	 * @return	| result == getOrderedFutureCollisions().first()
	 */
	public FutureCollision first() {
		return getOrderedFutureCollisions().first();
	}
	
	/**
	 * Return the date object reflecting when the first FutureCollision is going to take place.
	 * @return	| if(getBoundaryCollisionDate() == null && first() == null) result == null
	 * @return	| ( for each futureCollision in getOrderedFutureCollisions() 
	 * 			|		getFirstDate().after(futureCollision.getCollisionMoment()) == false )
	 * 			| && ( getFirstDate().after(getBoundaryCollisionDate()) == false )
	 */
	public Date getFirstDate() {
		if(getBoundaryCollisionDate() == null && first() == null)
			return null;
		if(getBoundaryCollisionDate() == null && first() != null)
			return first().getCollisionMoment();
		if(getBoundaryCollisionDate() != null && (first() == null || first().getCollisionMoment() == null)) 
			return getBoundaryCollisionDate();
		if(getBoundaryCollisionDate().before(first().getCollisionMoment()))
			return getBoundaryCollisionDate();
		else 
			return first().getCollisionMoment();
	}
	
	/**
	 * Return a FutureCollision object about the given object and the source of this collisionPredictor.
	 * @param object
	 * @return	| if(! getComponents().containsKey(object)) result == null
	 * 			| else result == getComponents().get(object)
	 */
	public FutureCollision get(SpaceObject object) {
		return getComponents().get(object);
	}
	
	/**
	 * Return the number of FutureCollisions in this collisionPredictor.
	 * @return	| result == getComponents().size()
	 */
	public int size() {
		return getComponents().size();
	}
	
	/**
	 * Put a new FutureCollision in this collisionPredictor.
	 * @param key
	 * @param value
	 * @post	| if(value.getObject1() != key && value.getObject2() != this)	(new this) == this
	 * 			| else (new this).getOrderedFutureCollisions().contains(value) == true && (new this).getComponents().containsKey(key)
	 * 			|	&& (new this).get(key) == value
	 */
	public void put(SpaceObject key, FutureCollision value) {
		getComponents().put(key, value);
		getOrderedFutureCollisions().add(value);
	}
	
	/**
	 * Remove the FutureCollision belonging to the given key from this collisionPredictor.
	 * @param key
	 * @post	| (new this).getOrderedFutureCollisions().contains(getComponents().get(key)) == false
	 * 			| (new this).getComponents().containsKey(key) == false
	 */
	public void remove(SpaceObject key) {
		FutureCollision value = getComponents().get(key);
		getComponents().remove(key); 
		getOrderedFutureCollisions().remove(value);
	}
	
	/**
	 * Remove the given FutureCollision from this collisionPredictor
	 * @param collision
	 * @post	| (new this).getOrderedFutureCollisions().contains(collision) == false
	 * 			| (new this).containsValue(collision) == false
	 */
	public void remove(FutureCollision collision) {
		getOrderedFutureCollisions().remove(collision);
		if(collision.getObject1() == getSource()) 
			getComponents().remove(collision.getObject2());
		else getComponents().remove(collision.getObject1());
	}
	
	/**
	 * Update all the FutureCollisions of this collisionPredictor belonging to one of the given objects.
	 * @param objects
	 * @post	| for each spaceObject in objects:
	 * 			|	if(get(spaceObject).isTerminated()) (new this).containsValue(get(spaceObject)) == false
	 * 			|	else (new this).get(spaceObject).getTimeToCollision() == 
	 * 			|					get(spaceObject).getObject1().getTimeToCollision(get(spaceObject).getObject2())		
	 */
	public void update(Collection<SpaceObject> objects) {
		for(SpaceObject object : objects) {
			FutureCollision collision = get(object);
			if(collision != null) {
				if(collision.isTerminated())
					remove(object);
				else {
					getOrderedFutureCollisions().remove(collision);
					collision.update();
					getOrderedFutureCollisions().add(collision);
				}
			}
		}		
	}
	
	/**
	 * Update all the FutureCollisions in this collisionPredictor.
	 * @effect	| update(getComponents().keySet())
	 */
	public void updateAll() {
		Collection<FutureCollision> toBeRemoved = new ArrayList<FutureCollision>();
		Collection<FutureCollision> toBeUpdated = new ArrayList<FutureCollision>();
		for(FutureCollision futureCollision :  getOrderedFutureCollisions()) {
			if(futureCollision.isTerminated())
				toBeRemoved.add(futureCollision);
			else { 
				toBeUpdated.add(futureCollision);
			}
		}
		for(FutureCollision fc : toBeRemoved) 
			remove(fc);
		for(FutureCollision fc : toBeUpdated) {
			getOrderedFutureCollisions().remove(fc);
			fc.update();
			getOrderedFutureCollisions().add(fc);
		}
	}

	/**
	 * Return an ordered set of all the futureCollisions in this collisionPredictor.
	 */
	@Basic
	public TreeSet<FutureCollision> getOrderedFutureCollisions() {
		return order;
	}
	
	/**
	 * Return a HashMap with the FutureCollisions and their key of this collisionPredictor.
	 */
	@Basic
	public HashMap<SpaceObject,FutureCollision> getComponents() {
		return this.components;
	}
	
	/**
	 * Return a boolean reflecting whether this collisionPredictor has valid FuruteCollisions.
	 * @return	| if (
	 * 			| 	for each futureCollision in getOrderedFutureCollisions():
	 * 			|		getComponents.containsValue(futureCollision) == true 
	 * 			|		futureCollision.getObject1() == source || futureCollision.getObject2() == source
	 * 			|	for each spaceObject in getComponents().keySet():
	 * 			|		get(spaceObject).getObject1() == spaceObject || get(spaceObject).getObject2() == spaceObject
	 * 			|		getOrderedFutureCollisions().contains(get(spaceObject))		)
	 * 			| 	result == true
	 * 			| else result == false
	 */
	public boolean hasProperFutureCollisions() {
		for(FutureCollision collision : getOrderedFutureCollisions()) {
			if (! getComponents().containsValue(collision)) return false;
			if (collision.getObject1() != getSource() &&  collision.getObject2() != getSource()) return false;
		}
		for(SpaceObject object : getComponents().keySet()) {
			if( get(object).getObject1() != getSource() && get(object).getObject2() != getSource()) return false;
			if(! getOrderedFutureCollisions().contains(get(object))) return false;
		}
		return true;
	}
}
